iT邦幫忙

2026 iThome 鐵人賽

DAY 26
0
自我挑戰組

用數據守護雙眼:生活型態對視力影響的探索性資料分析系列 第 26 篇

個人化臨床級解構!SHAP 瀑布圖與個體病例診斷

  • 分享至 

  • xImage
  •  

將 Day 25 的「全局群體歸因分析」推進至「個人化微觀診斷」層次。利用冠軍模型 LightGBM 與 SHAP 瀑布圖(Waterfall Plot)、單一樣本力導向圖,針對測試集中預測正確與預測邊界模糊的個別受試者,拆解模型從基礎基準值出發、各特徵如何像天平兩端正負推拉形成最終機率的完整決策路徑,產出具備醫療臨床價值的個人化用眼健康評估報告。

一、為什麼群體模型需要「單一樣本可解釋性」?
在 Day 25 中,我們看清了整個母體的大局觀 —— Eye_Dryness_Level、CESI_Score 與 Sleep_Hours 是全場三大主導特徵。但在臨床眼科諮詢或數位健康管理 App 中,使用者與醫師關心的永遠是:

「為什麼『我』會被判定為高風險?」
「是哪一項生活習慣把我推向了危險邊緣?如果我增加睡眠,風險能降低多少?」

SHAP的局部可解釋性正好解決了這個痛點:

  • 基準基準值(E[f(x)]):代表在「完全沒有受試者任何資訊」的狀況下,模型預設判定為高風險的先驗平均邊際數值。
  • 正向推力(> 0):哪些個人惡習或症狀(例如螢幕看太久、CESI 指數過高)將機率向上推升。
  • 負向拉力(< 0):哪些保護因子(例如充足睡眠、高頻率休息)正在拉住受試者,避免病情惡化。
  • 輸出值(f(x)):基礎值與所有個體特徵 SHAP 值的線性總和,經過 Softmax 轉換後即為最終預測機率。

今天將選出兩位具備強烈代表性的受試者進行深度個案病理解構:
案例 A(典型高風險個案):模型以極高把握度正確判定為高度疲勞風險。
案例 B(邊界模糊爭議個案):特徵處於模糊過渡帶,各特徵相互拉扯的複雜病例。

二、撰寫單一個案SHAP診斷與視覺化腳本
在 Colab 中新增儲存格,使用 shap.plots.waterfall 與客製化文字解析器產出個案報告:

# ==========================================
# Day 26:LightGBM 模型之單樣本 SHAP 瀑布圖診斷
# ==========================================

import warnings
warnings.filterwarnings('ignore')

import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.font_manager as fm
import pandas as pd
import numpy as np

import shap

# 1. 引用中文字型
font_path = 'NotoSansTC-Regular.otf'
my_font = fm.FontProperties(fname=font_path)

# 確保已繼承 Day 25 的 explainer 與 shap_values
# shap_values_high 針對 Class 2 (High Risk)
if 'shap_values_high' not in locals():
    explainer = shap.TreeExplainer(lgb_clf)
    shap_values = explainer(X_test)
    shap_values_high = shap_values[:, :, 2]

# 2. 挑選代表性病例
# 取得測試集真實標籤為 High (2) 且模型精準預測為 High 的樣本
y_pred_lgb = lgb_clf.predict(X_test)
y_prob_lgb = lgb_clf.predict_proba(X_test)

correct_high_indices = np.where((y_test.values == 2) & (y_pred_lgb == 2))[0]
# 選取高風險預測機率最高的一位作為典型案例
case_a_idx = correct_high_indices[np.argmax(y_prob_lgb[correct_high_indices, 2])]

# 選取一位真實為 High 但預測機率接近邊界 (約 0.35~0.45) 的邊界爭議案例
borderline_indices = np.where((y_test.values == 2) & (y_pred_lgb != 2))[0]
case_b_idx = borderline_indices[0] if len(borderline_indices) > 0 else 0

# 3. 繪製案例 A 瀑布圖 (典型高風險個案)
plt.figure(figsize=(10, 6))
plt.title(f'【案例 A】典型高風險受試者 (測試集樣本 #{case_a_idx}) SHAP 決策瀑布圖', fontproperties=my_font, fontsize=13, fontweight='bold', pad=15)
shap.plots.waterfall(shap_values_high[case_a_idx], max_display=10, show=False)
plt.xlabel('模型 Log-Odds 輸出值 (往右代表推升至高風險)', fontproperties=my_font, fontsize=11)
plt.tight_layout()
plt.show()

# 4. 繪製案例 B 瀑布圖 (邊界爭議受試者)
plt.figure(figsize=(10, 6))
plt.title(f'【案例 B】邊界拉鋸受試者 (測試集樣本 #{case_b_idx}) SHAP 決策瀑布圖', fontproperties=my_font, fontsize=13, fontweight='bold', pad=15)
shap.plots.waterfall(shap_values_high[case_b_idx], max_display=10, show=False)
plt.xlabel('模型 Log-Odds 輸出值 (往右代表推升至高風險)', fontproperties=my_font, fontsize=11)
plt.tight_layout()
plt.show()

# 5. 建立文字化臨床診斷處方輸出器
def generate_clinical_report(sample_idx, case_name):
    sv = shap_values_high[sample_idx]
    orig_features = X_test.iloc[sample_idx]
    high_prob = y_prob_lgb[sample_idx, 2]
    pred_label = ['低度 (Low)', '中度 (Medium)', '高度 (High)'][y_pred_lgb[sample_idx]]
    true_label = ['低度 (Low)', '中度 (Medium)', '高度 (High)'][y_test.iloc[sample_idx]]
    
    print(f"==================================================")
    print(f"  {case_name}:臨床級用眼疲勞 AI 診斷處方箋")
    print(f"==================================================")
    print(f"• 測試集樣本索引:#{sample_idx}")
    print(f"• 真實風險等級:{true_label}")
    print(f"• 模型預測等級:{pred_label} (高度風險判定機率: {high_prob*100:.2f}%)")
    print(f"• 基礎基準期望值 E[f(x)]:{sv.base_values:.3f} | 最終模型輸出 f(x):{sv.values.sum() + sv.base_values:.3f}\n")
    
    # 提取推進與壓制的前三大特徵
    contrib_series = pd.Series(sv.values, index=orig_features.index).sort_values(ascending=False)
    
    top_push = contrib_series.head(3)
    top_pull = contrib_series.tail(3).sort_values(ascending=True)
    
    print("【主要致病推升因子 (Top Risk Drivers)】:")
    for feat, val in top_push.items():
        if val > 0:
            print(f"  ▲ {feat:<28} 貢獻度: +{val:.4f}")
            
    print("\n【主要保護抑制因子 (Top Protective Factors)】:")
    for feat, val in top_pull.items():
        if val < 0:
            print(f"  ▼ {feat:<28} 貢獻度: {val:.4f}")
    print("==================================================\n")

generate_clinical_report(case_a_idx, "案例 A (典型高風險)")
generate_clinical_report(case_b_idx, "案例 B (邊界拉鋸個案)")

https://ithelp.ithome.com.tw/upload/images/20261010/20178794FsoztgMSr6.png
https://ithelp.ithome.com.tw/upload/images/20261010/20178794xvVrwqLsSN.png
https://ithelp.ithome.com.tw/upload/images/20261010/20178794swzWZzmEMc.png


上一篇
揭開黑盒子!SHAP 賽局理論特徵解釋與模型全局歸因分析
系列文
用數據守護雙眼:生活型態對視力影響的探索性資料分析 共 26 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言